#!/usr/bin/env python3
# Copyright (c) 2008-11 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. It is provided for educational
# purposes and is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import locale
locale.setlocale(locale.LC_ALL, "")

import datetime
import gzip
import optparse
import os
import pickle
import re
import struct
import sys
import textwrap

USE_RESTRICTIVE_P_FORMAT = False
USE_LONG_WINDED_IMPORT_FUNCTION = False

GZIP_MAGIC = b"\x1F\x8B"


class IncidentError(Exception): pass


class Incident:

    def __init__(self, report_id, date, airport, aircraft_id,
                 aircraft_type, pilot_percent_hours_on_type,
                 pilot_total_hours, midair, narrative=""):
        """
        >>> kwargs = dict(report_id="2007061289X")
        >>> kwargs["date"] = datetime.date(2007, 6, 12)
        >>> kwargs["airport"] = "Los Angeles"
        >>> kwargs["aircraft_id"] = "8184XK"
        >>> kwargs["aircraft_type"] = "CVS91"
        >>> kwargs["pilot_percent_hours_on_type"] = 17.5
        >>> kwargs["pilot_total_hours"] = 1258
        >>> kwargs["midair"] = False
        >>> incident = Incident(**kwargs)
        >>> incident.report_id, incident.date, incident.airport
        ('2007061289X', datetime.date(2007, 6, 12), 'Los Angeles')
        >>> incident.aircraft_id, incident.aircraft_type, incident.midair
        ('8184XK', 'CVS91', False)
        >>> incident.pilot_percent_hours_on_type, incident.pilot_total_hours
        (17.5, 1258)
        >>> incident.approximate_hours_on_type
        220
        >>> incident.narrative = "Two different\\nlines of text"
        >>> incident.date = datetime.date(2007, 6, 13)
        >>> str(incident)
        "Incident('2007061289X', datetime.date(2007, 6, 13), 'Los Angeles', '8184XK', 'CVS91', 17.5, 1258, False, '''Two different\\nlines of text''')"
        >>> kwargs["report_id"] = "fail"
        >>> incident = Incident(**kwargs)
        Traceback (most recent call last):
        ...
        AssertionError: invalid report ID
        """
        assert len(report_id) >= 8 and len(report_id.split()) == 1, \
               "invalid report ID"
        self.__report_id = report_id
        self.date = date
        self.airport = airport
        self.aircraft_id = aircraft_id
        self.aircraft_type = aircraft_type
        self.pilot_percent_hours_on_type = pilot_percent_hours_on_type
        self.pilot_total_hours = pilot_total_hours
        self.midair = midair
        self.narrative = narrative


    @property
    def report_id(self):
        return self.__report_id

    @property
    def date(self):
        "The incident date"
        return self.__date

    @date.setter
    def date(self, date):
        assert isinstance(date, datetime.date), "invalid date"
        self.__date = date


    @property
    def pilot_percent_hours_on_type(self):
        "The percentage of total hours flown on this aircraft type"
        return self.__pilot_percent_hours_on_type

    @pilot_percent_hours_on_type.setter
    def pilot_percent_hours_on_type(self, percent):
        assert 0.0 <= percent <= 100.0, "out of range percentage"
        self.__pilot_percent_hours_on_type = percent


    @property
    def pilot_total_hours(self):
        "The total hours this pilot has flown"
        return self.__pilot_total_hours

    @pilot_total_hours.setter
    def pilot_total_hours(self, hours):
        assert hours > 0, "invalid number of hours"
        self.__pilot_total_hours = hours


    @property
    def approximate_hours_on_type(self):
        return int(self.__pilot_total_hours *
                   (self.__pilot_percent_hours_on_type / 100))


    @property
    def midair(self):
        "Whether the incident involved another aircraft"
        return self.__midair

    @midair.setter
    def midair(self, value):
        assert isinstance(value, bool), "invalid midair value"
        self.__midair = value


    @property
    def airport(self):
        "The incident's airport"
        return self.__airport

    @airport.setter
    def airport(self, airport):
        assert airport and "\n" not in airport, "invalid airport"
        self.__airport = airport


    @property
    def aircraft_id(self):
        "The aircraft ID"
        return self.__aircraft_id

    @aircraft_id.setter
    def aircraft_id(self, aircraft_id):
        assert aircraft_id and "\n" not in aircraft_id, \
               "invalid aircraft ID"
        self.__aircraft_id = aircraft_id


    @property
    def aircraft_type(self):
        "The aircraft type"
        return self.__aircraft_type

    @aircraft_type.setter
    def aircraft_type(self, aircraft_type):
        assert aircraft_type and "\n" not in aircraft_type, \
               "invalid aircraft type"
        self.__aircraft_type = aircraft_type


    @property
    def narrative(self):
        "The incident's narrative"
        return self.__narrative

    @narrative.setter
    def narrative(self, narrative):
        self.__narrative = narrative


    def __repr__(self):
        return ("Incident({0.report_id!r}, {0.date!r}, "
                "{0.airport!r}, {0.aircraft_id!r}, "
                "{0.aircraft_type!r}, "
                "{0.pilot_percent_hours_on_type!r}, "
                "{0.pilot_total_hours!r}, {0.midair!r}, "
                "'''{0.narrative}''')".format(self))


class IncidentCollection(dict):

    """
    >>> kwargs = dict(report_id="2007061289X")
    >>> kwargs["date"] = datetime.date(2007, 6, 12)
    >>> kwargs["airport"] = "Los Angeles"
    >>> kwargs["aircraft_id"] = "8184XK"
    >>> kwargs["aircraft_type"] = "CVS91"
    >>> kwargs["pilot_percent_hours_on_type"] = 17.5
    >>> kwargs["pilot_total_hours"] = 1258
    >>> kwargs["midair"] = False
    >>> incidents = IncidentCollection()
    >>> incident = Incident(**kwargs)
    >>> incidents[incident.report_id] = incident
    >>> kwargs["report_id"] = "2007061989K"
    >>> kwargs["date"] = datetime.date(2007, 6, 19)
    >>> kwargs["pilot_percent_hours_on_type"] = 20
    >>> kwargs["pilot_total_hours"] = 17521
    >>> incident = Incident(**kwargs)
    >>> incidents[incident.report_id] = incident
    >>> kwargs["report_id"] = "2007052989V"
    >>> kwargs["date"] = datetime.date(2007, 5, 29)
    >>> kwargs["pilot_total_hours"] = 1875
    >>> incident = Incident(**kwargs)
    >>> incidents[incident.report_id] = incident
    >>> for incident in incidents.values():
    ...     print(incident.report_id, incident.date.isoformat())
    2007052989V 2007-05-29
    2007061289X 2007-06-12
    2007061989K 2007-06-19
    >>> for report_id in reversed(incidents):
    ...     print(report_id, incidents[report_id].date.isoformat())
    2007061989K 2007-06-19
    2007061289X 2007-06-12
    2007052989V 2007-05-29
    """

    def values(self):
        for report_id in self.keys():
            yield self[report_id]


    def items(self):
        for report_id in self.keys():
            yield (report_id, self[report_id])


    def __iter__(self):
        for report_id in sorted(super().keys()):
            yield report_id

    keys = __iter__


    def __reversed__(self):
        for report_id in sorted(super().keys(), reverse=True):
            yield report_id


    def export(self, filename, writer=None, compress=False):
        extension = os.path.splitext(filename)[1].lower()
        if extension == ".ait":
            return self.export_text(filename)
        elif extension == ".aip":
            return self.export_pickle(filename, compress)


    def import_(self, filename, reader=None):
        extension = os.path.splitext(filename)[1].lower()
        call = {(".ait", "manual"): self.import_text_manual,
                (".aip", None): self.import_pickle}
        result = call[extension, reader](filename)
        if result == False:
            self.clear()
        return result

    if USE_LONG_WINDED_IMPORT_FUNCTION:
        def import_(self, filename, reader=None):
            extension = os.path.splitext(filename)[1].lower()
            result = False
            if extension == ".ait":
                if reader == "manual":
                    result = self.import_text_manual(filename)
            elif extension == ".aip":
                result = self.import_pickle(filename)
            if result == False:
                self.clear()
            return result


    def export_text(self, filename):
        wrapper = textwrap.TextWrapper(initial_indent="    ",
                                       subsequent_indent="    ")
        fh = None
        try:
            fh = open(filename, "w", encoding="utf8")
            for incident in self.values():
                narrative = "\n".join(wrapper.wrap(
                                   incident.narrative.strip()))
                fh.write("[{0.report_id}]\n"
                         "date={0.date!s}\n"
                         "aircraft_id={0.aircraft_id}\n"
                         "aircraft_type={0.aircraft_type}\n"
                         "airport={airport}\n"
                         "pilot_percent_hours_on_type="
                         "{0.pilot_percent_hours_on_type}\n"
                         "pilot_total_hours={0.pilot_total_hours}\n"
                         "midair={0.midair:d}\n"
                         ".NARRATIVE_START.\n{narrative}\n"
                         ".NARRATIVE_END.\n\n".format(incident,
                    airport=incident.airport.strip(),
                    narrative=narrative))
            return True
        except EnvironmentError as err:
            print("{0}: export error: {1}".format(
                  os.path.basename(sys.argv[0]), err))
            return False
        finally:
            if fh is not None:
                fh.close()


    def import_text_manual(self, filename):
        fh = None
        try:
            fh = open(filename, encoding="utf8")
            self.clear()
            data = {}
            narrative = None
            for lino, line in enumerate(fh, start=1):
                line = line.rstrip()
                if not line and narrative is None:
                    continue
                if narrative is not None:
                    if line == ".NARRATIVE_END.":
                        data["narrative"] = textwrap.dedent(
                                                    narrative).strip()
                        if len(data) != 9:
                            raise IncidentError("missing data on "
                                            "line {0}".format(lino))
                        incident = Incident(**data)
                        self[incident.report_id] = incident
                        data = {}
                        narrative = None
                    else:
                        narrative += line + "\n"
                elif (not data and line[0] == "["
                               and line[-1] == "]"):
                    data["report_id"] = line[1:-1]
                elif "=" in line:
                    key, value = line.split("=", 1)
                    if key == "date":
                        data[key] = datetime.datetime.strptime(value,
                                                    "%Y-%m-%d").date()
                    elif key == "pilot_percent_hours_on_type":
                        data[key] = float(value)
                    elif key == "pilot_total_hours":
                        data[key] = int(value)
                    elif key == "midair":
                        data[key] = bool(int(value))
                    else:
                        data[key] = value
                elif line == ".NARRATIVE_START.":
                    narrative = ""
                else:
                    raise KeyError("parsing error on line {0}".format(
                                   lino))
            return True
        except (EnvironmentError, ValueError, KeyError,
                IncidentError) as err:
            print("{0}: import error: {1}".format(
                  os.path.basename(sys.argv[0]), err))
            return False
        finally:
            if fh is not None:
                fh.close()


    def export_pickle(self, filename, compress=False):
        fh = None
        try:
            if compress:
                fh = gzip.open(filename, "wb")
            else:
                fh = open(filename, "wb")
            pickle.dump(self, fh, pickle.HIGHEST_PROTOCOL)
            return True
        except (EnvironmentError, pickle.PicklingError) as err:
            print("{0}: export error: {1}".format(
                  os.path.basename(sys.argv[0]), err))
            return False
        finally:
            if fh is not None:
                fh.close()


    def import_pickle(self, filename):
        fh = None
        try:
            fh = open(filename, "rb")
            magic = fh.read(len(GZIP_MAGIC))
            if magic == GZIP_MAGIC:
                fh.close()
                fh = gzip.open(filename, "rb")
            else:
                fh.seek(0)
            self.clear()
            self.update(pickle.load(fh))
            return True
        except (EnvironmentError, pickle.UnpicklingError) as err:
            print("{0}: import error: {1}".format(
                  os.path.basename(sys.argv[0]), err))
            return False
        finally:
            if fh is not None:
                fh.close()


if __name__ == "__main__":
    import doctest
    doctest.testmod()
